﻿using System;
using FarseerGames.FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using FarseerGames.FarseerPhysics.Collisions;
using UnitLibrary.Utility;

namespace UnitLibrary.Managers
{
    public class Camera2D
    {
        Vector2 _position;
        Vector2 _origPosition;
        Vector2 _targetPosition;
        float _moveSpeed;
        float _rotation;
        float _origRotation;
        float _targetRotation;
        float _zoom;
        float _origZoom;
        float _targetZoom;
        float _zoomRate;
        float _maxZoom;
        float _minZoom;
        float _rotationRate;
        float _transition;
        bool _transitioning;
        const float _transitionSpeed = 0.01f;
        const float _smoothingSpeed = 0.15f;
        Vector2 _size;
        Vector2 _minPosition;
        Vector2 _maxPosition;
        Body _trackingBody;

        Func<Camera2D, bool> _clampingEnabled = (Camera2D camera) =>
        {
            return (camera._minPosition != camera._maxPosition);
        };
#if(XBOX)
        Func<InputHelper, bool> _zoomIn = (InputHelper input) =>
        {
            return input.IsCurPress(Buttons.DPadUp);
        };
        Func<InputHelper, bool> _zoomOut = (InputHelper input) =>
        {
            return input.IsCurPress(Buttons.DPadDown);
        };

        //Controls for camera if using XBOX
        Func<InputHelper, Camera2D, float> _horizontalCameraMovement =
            (InputHelper input, Camera2D camera) =>
        {
            return (input.RightStickPosition.X * camera._moveSpeed) * camera._zoom;
        };

        Func<InputHelper, Camera2D, float> _verticalCameraMovement =
            (InputHelper input, Camera2D camera) =>
        {
            return (-input.RightStickPosition.Y * camera._moveSpeed) * camera.Zoom;
        };

        Func<InputHelper, bool> _rotateLeft = (InputHelper input) =>
        {
            return false;
        };

        Func<InputHelper, bool> _rotateRight = (InputHelper input) =>
        {
            return false;
        };

        Func<InputHelper, bool> _resetCamera = (InputHelper input) =>
        {
            return input.IsCurPress(Buttons.RightStick);
        };
#else
        //Controls for camera if using PC

        Func<InputHelper, bool> _zoomIn = (InputHelper input) =>
        {
            if (input.MouseScrollWheelVelocity > 0f) return true;
            else return false;
        };

        Func<InputHelper, bool> _zoomOut = (InputHelper input) =>
        {
            if (input.MouseScrollWheelVelocity < 0f) return true;
            else return false;
        };

        Func<InputHelper, Camera2D, float> _horizontalCameraMovement =
            (InputHelper input, Camera2D camera) =>
        {
                if(input.IsCurPress(Keys.A))
                    return (-1f * camera._moveSpeed * camera._zoom);
                if(input.IsCurPress(Keys.D))
                    return (1f * camera._moveSpeed * camera._zoom);
                else
                    return 0f;
        };

        Func<InputHelper, Camera2D, float> _verticalCameraMovement =
            (InputHelper input, Camera2D camera) =>
        {
            if (input.IsCurPress(Keys.W))
                return (-1f * camera._moveSpeed * camera._zoom);
            if (input.IsCurPress(Keys.S))
                return (1f * camera._moveSpeed * camera._zoom);
            else
                return 0f;
        };

        Func<InputHelper, bool> _rotateLeft = (InputHelper input) =>
        {
            if(input.IsCurPress(Keys.Z))
                return true;
            else
                return false;
        };

        Func<InputHelper, bool> _rotateRight = (InputHelper input) =>
        {
            if(input.IsCurPress(Keys.C))
                return true;
            else
                return false;
        };

        Func<InputHelper, bool> _resetCamera = (InputHelper input) =>
        {
            return input.IsCurPress(Keys.X);            
        };
#endif

        /// <summary>
        /// The primary constructor for the Camera2D class.
        /// </summary>
        /// <param name="size">
        /// the size of the camera's view when at zoom = 1. This is usually
        /// set to the viewport's size.
        /// </param>
        /// <param name="position">
        /// The starting position of the camera. Default is (0,0).
        /// </param>
        /// <param name="moveSpeed">
        /// The amount that the camera moves in one timestep. Default is 1.
        /// </param>
        /// <param name="rotation">
        /// The starting rotation of the camera. Default is 0.
        /// </param>
        /// <param name="rotationRate">
        /// The amount that the camera rotates (in radians) in one timestep.
        /// Default is 0.01f.
        /// </param>
        /// <param name="zoom">
        /// The starting zoom of the camera. Default is 1.
        /// </param>
        /// <param name="zoomRate">
        /// The amount that the camera zooms in or out in one timestep.
        /// Default is 0.01f.
        /// </param>
        /// <param name="minZoom">
        /// The futhest zoomed out that the camera can be. Smaller numbers
        /// are further zoomed out. Default is 0.25f (4x regular size).
        /// </param>
        /// <param name="maxZoom">
        /// the furthest zoomed in the camera can be. Larger numbers
        /// are further zoomed in. Default is 4f (0.25x regular size).
        /// </param>
        /// <param name="minPosition">
        /// The furthest up, and the furthest left the camera can go.
        /// Default is (0, 0). if this value equals maxPosition, then
        /// no clamping will be applied (unless you override that function).
        /// </param>
        /// <param name="maxPosition">
        /// the furthest down, and the furthest right the camera will go.
        /// Default is (0, 0). if this value equals minPosition, then no
        /// clamping will be applied (unless you override that function).
        /// </param>
        public Camera2D(Vector2 size,
                        Vector2? position,
                        float? moveSpeed,
                        float? rotation,
                        float? rotationRate,
                        float? zoom,
                        float? zoomRate,
                        float? minZoom,
                        float? maxZoom,
                        Vector2? minPosition,
                        Vector2? maxPosition)
        {
            _position = (position.HasValue) ? position.Value : Vector2.Zero;
            _origPosition = (position.HasValue) ? position.Value : Vector2.Zero;
            _targetPosition = (position.HasValue) ? position.Value : Vector2.Zero;
            _moveSpeed = (moveSpeed.HasValue) ? moveSpeed.Value : 1;
            _rotation = (rotation.HasValue) ? rotation.Value : 0;
            _origRotation = (rotation.HasValue) ? rotation.Value : 0;
            _targetRotation = (rotation.HasValue) ? rotation.Value : 0;
            _rotationRate = (rotationRate.HasValue) ? rotationRate.Value : 0.01f;
            _zoom = (zoom.HasValue) ? zoom.Value : 1;
            _origZoom = (zoom.HasValue) ? zoom.Value : 1;
            _targetZoom = (zoom.HasValue) ? zoom.Value : 1;
            _zoomRate = (zoomRate.HasValue) ? zoomRate.Value : 0.01f;
            _minZoom = (minZoom.HasValue) ? minZoom.Value : 0.25f;
            _maxZoom = (maxZoom.HasValue) ? maxZoom.Value : 4f;
            _size = size;
            _minPosition = (minPosition.HasValue) ? minPosition.Value : Vector2.Zero;
            _maxPosition = (maxPosition.HasValue) ? maxPosition.Value : Vector2.Zero;
        }

        /// <summary>
        /// The current position of the camera.
        /// </summary>
        public Vector2 Position
        {
            get { return _position; }
            set { _position = value; }
        }
        /// <summary>
        /// The current rotation of the camera in radians.
        /// </summary>
        public float Rotation
        {
            get { return _rotation; }
            set { _rotation = value; }
        }
        /// <summary>
        /// The current zoom of the camera. This is a value indicating
        /// how far zoomed in or out the camera is. To get the actual
        /// current size of the camera view, see CurSize.
        /// </summary>
        public float Zoom
        {
            get { return _zoom; }
            set { _zoom = value; }
        }
        /// <summary>
        /// the furthest zoomed in the camera can be. Larger numbers
        /// are further zoomed in.
        /// </summary>
        public float MaxZoom
        {
            get { return _maxZoom; }
            set { _maxZoom = value; }
        }
        /// <summary>
        /// The futhest zoomed out that the camera can be. Smaller numbers
        /// are further zoomed out.
        /// </summary>
        public float MinZoom
        {
            get { return _minZoom; }
            set { _minZoom = value; }
        }
        /// <summary>
        /// The amount that the camera rotates (in radians) in one timestep.
        /// </summary>
        public float RotationRate
        {
            get { return _rotationRate; }
            set { _rotationRate = value; }
        }
        /// <summary>
        /// The amount that the camera zooms in or out in one timestep.
        /// </summary>
        public float ZoomRate
        {
            get { return _zoomRate; }
            set { _zoomRate = value; }
        }
        /// <summary>
        /// A vector representing the size of the camera when zoom is at 1.
        /// </summary>
        public Vector2 Size
        {
            get { return _size; }
            //i think changing the camera size on the fly
            //would screw stuff up
            //set { _size = value; }
        }
        /// <summary>
        /// a vector representing the current size of the camera view.
        /// Expressed as: Size * (1 / zoom).
        /// </summary>
        public Vector2 CurSize
        {
            get { return Vector2.Multiply(_size, 1 / _zoom); }
        }
        /// <summary>
        /// A matrix representing the camera's current position, rotation, and zoom.
        /// Feed this to SpriteBatch.Begin (or use it in your matrix calculations for
        /// drawing 2D polygons)
        /// </summary>
        public Matrix CameraMatrix
        {
            get
            {
                return Matrix.Identity *
                      Matrix.CreateTranslation(new Vector3(-_position, 0)) *
                      Matrix.CreateScale(_zoom) *
                      Matrix.CreateRotationZ(_rotation) *
                      Matrix.CreateTranslation(new Vector3(_size / 2, 0));
            }
        }
        /// <summary>
        /// The furthest up, and the furthest left the camera can go.
        /// if this value equals maxPosition, then no clamping will be
        /// applied (unless you override that function).
        /// </summary>
        public Vector2 MinPosition
        {
            get { return _minPosition; }
            set { _minPosition = value; }
        }
        /// <summary>
        /// the furthest down, and the furthest right the camera will go.
        /// if this value equals minPosition, then no clamping will be
        /// applied (unless you override that function).
        /// </summary>
        public Vector2 MaxPosition
        {
            get { return _maxPosition; }
            set { _maxPosition = value; }
        }
        /// <summary>
        /// the body that this camera is currently tracking.
        /// Null if not tracking any.
        /// </summary>
        public Body TrackingBody
        {
            get { return _trackingBody; }
            set { _trackingBody = value; }
        }
        /// <summary>
        /// a function that is called to determine if the user wants
        /// to zoom in.
        /// </summary>
        public Func<InputHelper, bool> ZoomIn
        {
            get { return _zoomIn; }
            set { _zoomIn = value; }
        }
        /// <summary>
        /// a function that is called to determine whether the user wants
        /// to zoom out.
        /// </summary>
        public Func<InputHelper, bool> ZoomOut
        {
            get { return _zoomOut; }
            set { _zoomOut = value; }
        }
        /// <summary>
        /// a function that determines whether clamping is currently enabled
        /// for this camera.
        /// </summary>
        public Func<Camera2D, bool> ClampingEnabled
        {
            get { return _clampingEnabled; }
            set { _clampingEnabled = value; }
        }
        /// <summary>
        /// a function that is called to determine the amount of horizontal
        /// movement that the user is requesting that the camera be moved
        /// by.
        /// </summary>
        public Func<InputHelper, Camera2D, float> HorizontalCameraMovement
        {
            get { return _horizontalCameraMovement; }
            set { _horizontalCameraMovement = value; }
        }
        /// <summary>
        /// a function that is called to determine the amount of vertical
        /// movement that the user is requesting that the camera be moved
        /// by.
        /// </summary>
        public Func<InputHelper, Camera2D, float> VerticalCameraMovement
        {
            get { return _verticalCameraMovement; }
            set { _verticalCameraMovement = value; }
        }
        /// <summary>
        /// a function that is called to determine if the user wants to
        /// rotate the camera left.
        /// </summary>
        public Func<InputHelper, bool> RotateLeft
        {
            get { return _rotateLeft; }
            set { _rotateLeft = value; }
        }
        /// <summary>
        /// a function that is called to determine if the user wants to rotate
        /// the camera right.
        /// </summary>
        public Func<InputHelper, bool> RotateRight
        {
            get { return _rotateRight; }
            set { _rotateRight = value; }
        }
        /// <summary>
        /// A function that is called to determine if the user is requesting
        /// that the camera be reset to it's original parameters.
        /// </summary>
        public Func<InputHelper, bool> ResetCamera
        {
            get { return _resetCamera; }
            set { _resetCamera = value; }
        }

        /// <summary>
        /// Moves the camera forward one timestep.
        /// </summary>
        /// <param name="input">
        /// the an InputHelper input representing the current
        /// input state.
        /// </param>
        public void Update(InputHelper input)
        {
            if (!_transitioning)
            {
                if (_trackingBody == null)
                {
                    if (_clampingEnabled(this))
                        _targetPosition = Vector2.Clamp(_position + new Vector2(
                                _horizontalCameraMovement(input, this),
                                _verticalCameraMovement(input, this)),
                                _minPosition,
                                _maxPosition);
                    else
                        _targetPosition += new Vector2(
                            _horizontalCameraMovement(input, this),
                            _verticalCameraMovement(input, this));
                }
                else
                {
                    if (_clampingEnabled(this))
                        _targetPosition = Vector2.Clamp(
                            _trackingBody.Position,
                            _minPosition,
                            _maxPosition);
                    else
                        _targetPosition = _trackingBody.Position;
                }
                if (_zoomIn(input))
                    _targetZoom = Math.Min(_maxZoom, _zoom + _zoomRate);
                if (_zoomOut(input))
                    _targetZoom = Math.Max(_minZoom, _zoom - _zoomRate);
                //these might need to be swapped
                if (_rotateLeft(input))
                    _targetRotation = (_rotation + _rotationRate) % (float)(Math.PI * 2);
                if (_rotateRight(input))
                    _targetRotation = (_rotation - _rotationRate) % (float)(Math.PI * 2);
                if (input.IsCurPress(Buttons.RightStick))
                {
                    _transitioning = true;
                    _targetPosition = _origPosition;
                    _targetRotation = _origRotation;
                    _targetZoom = _origZoom;
                    _trackingBody = null;
                }
            }
            else if (_transition < 1)
            {
                _transition += _transitionSpeed;
            }
            if (_transition >= 1f ||
                (_position == _origPosition &&
                _rotation == _origRotation &&
                _zoom == _origZoom))
            {
                _transition = 0;
                _transitioning = false;
            }
            _position = Vector2.SmoothStep(_position, _targetPosition, _smoothingSpeed);
            _rotation = MathHelper.SmoothStep(_rotation, _targetRotation, _smoothingSpeed);
            _zoom = MathHelper.SmoothStep(_zoom, _targetZoom, _smoothingSpeed);
        }

        /// <summary>
        /// tells you if drawing the texture will actually draw onscreen.
        /// </summary>
        /// <param name="tex">
        /// the texture to check.
        /// </param>
        /// <param name="position">
        /// the position of the texture's center.
        /// </param>
        /// <param name="origin">
        /// a Vector2 equaling half of the texture's size.
        /// </param>
        /// <param name="rotation">
        /// the rotation of the texture, in radians.
        /// </param>
        /// <returns>
        /// a bool indicating whether you should draw this texture.
        /// </returns>
        public bool ShouldDraw(Texture2D tex, Vector2 position, Vector2 origin, float rotation)
        {
            Matrix textureMatrix = Matrix.CreateRotationZ(rotation) * Matrix.CreateTranslation(new Vector3(position, 0));
            Vector2 topLeft = Vector2.Transform(-origin, textureMatrix);
            Vector2 topRight = Vector2.Transform(new Vector2(origin.X, -origin.Y), textureMatrix);
            Vector2 botLeft = Vector2.Transform(new Vector2(-origin.X, origin.Y), textureMatrix);
            Vector2 botRight = Vector2.Transform(origin, textureMatrix);
            AABB texAABB = new AABB();
            if (rotation >= 0 && rotation <= MathHelper.PiOver2)
                texAABB = new AABB(new Vector2(botLeft.X, topLeft.Y), new Vector2(topRight.X, botRight.Y));
            else if (rotation > MathHelper.PiOver2 && rotation <= MathHelper.Pi)
                texAABB = new AABB(new Vector2(botRight.X, botLeft.Y), new Vector2(topLeft.X, topRight.Y));
            else if (rotation > MathHelper.Pi && rotation <= (MathHelper.Pi + MathHelper.PiOver2))
                texAABB = new AABB(new Vector2(botRight.X, topRight.Y), new Vector2(botLeft.X, topLeft.Y));
            else if (rotation > (MathHelper.Pi + MathHelper.PiOver2) && rotation <= MathHelper.TwoPi)
                texAABB = new AABB(new Vector2(topLeft.X, topRight.Y), new Vector2(botRight.X, botLeft.Y));


            Matrix simpleCameraMatrix = Matrix.Identity *
                Matrix.CreateTranslation(new Vector3(-_position, 0)) *
                Matrix.CreateRotationZ(_rotation) *
                Matrix.CreateTranslation(new Vector3(-_size / 2, 0));
            Vector2 camOrigin = (_size * (1 / _zoom));
            topLeft = Vector2.Transform(-camOrigin, simpleCameraMatrix);
            topRight = Vector2.Transform(new Vector2(camOrigin.X, -camOrigin.Y), simpleCameraMatrix);
            botLeft = Vector2.Transform(new Vector2(-camOrigin.X, camOrigin.Y), simpleCameraMatrix);
            botRight = Vector2.Transform(camOrigin, simpleCameraMatrix);
            AABB camAABB = new AABB();
            if (rotation >= 0 && rotation <= MathHelper.PiOver2)
                camAABB = new AABB(new Vector2(botLeft.X, topLeft.Y), new Vector2(topRight.X, botRight.Y));
            else if (rotation > MathHelper.PiOver2 && rotation <= MathHelper.Pi)
                camAABB = new AABB(new Vector2(botRight.X, botLeft.Y), new Vector2(topLeft.X, topRight.Y));
            else if (rotation > MathHelper.Pi && rotation <= (MathHelper.Pi + MathHelper.PiOver2))
                camAABB = new AABB(new Vector2(botRight.X, topRight.Y), new Vector2(botLeft.X, topLeft.Y));
            else if (rotation > (MathHelper.Pi + MathHelper.PiOver2) && rotation <= MathHelper.TwoPi)
                camAABB = new AABB(new Vector2(topLeft.X, topRight.Y), new Vector2(botRight.X, botLeft.Y));

            if (camAABB.Contains(texAABB.Min) || camAABB.Contains(texAABB.Max))
                return true;
            return false;
        }

        /// <summary>
        /// Function to translate screen coordinates to game world coordinates
        /// </summary>
        /// <param name="position"></param>
        /// <returns></returns>
        public Vector2 ToWorldLocation(Vector2 position)
        {
            return Vector2.Transform(position, Matrix.Invert(CameraMatrix));
        }
    }
}
    
